feat(cli): announce seed settlement on serve's ipc channel and forward it from os dev - #17892
Conversation
…d it from dev `✓ Server is ready` is true about the HTTP server and silent about the app. Past the inline seed budget the rest of the seed runs detached, so the banner — and any parent that waits for it — can be eighty seconds ahead of a hundred-line error wall, and nothing a parent can observe distinguishes that boot from one that seeded cleanly. `serve` now sends a second ipc message, `objectstack:seed-settled`, beside `objectstack:listening`, latched so it can never precede it; `os dev` relays it verbatim to its own parent when one holds the channel. The producer is not new: the runtime already publishes the tally under the spec's `seed-settlement` contract, and this is the hop outward. Keyed on `inFlight === 0`, not `pending === 0`: multi-tenant replay and `skipSeedData` keep `pending` above zero for the life of the boot, so a `pending`-keyed message would never be sent there and its absence would be indistinguishable from a boot still writing. Those boots get the message with `suppressed` reasons instead. The banner's over-budget path now says `Seeds: pending` and that seeding continues in the background, so the transcript is no longer byte-identical to a boot that declared no seeds. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
…nner rider 64 assertions across three files: the message reaches process.send and stays silent without a channel; the latch cannot let it overtake `objectstack:listening` and cannot send twice; the over-budget path withholds then speaks; both suppressed modes announce with their reason rather than hanging; a kernel with no seed pipeline counts as settled; `os dev` relays verbatim and claims only its own message; and the banner's over-budget row, with the ablation showing the same boot without the reading is the byte-identical no-seeds transcript. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
Adds a `Waiting for the boot from a parent process` section to the `os dev` page: what each message means, the spawn-with-ipc snippet, the asymmetry (`os dev` consumes `objectstack:listening` and relays only the settle message), and what `objectstack:seed-settled` promises — including why it is sent on suppressed boots with a reason rather than withheld. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
📓 Docs Drift CheckThis PR changes 1 package(s): 32 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 24570cabd1b8eae6d9dfc0ba5273b93ad02df4e6 && git checkout 24570cabd1b8eae6d9dfc0ba5273b93ad02df4e6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c744c0af332c31748ab7efc87e87505277adae2d 225ad588f7ac6ff238f6e770d8ab5ee59cf2d9de && git checkout -B drift-repro c744c0af332c31748ab7efc87e87505277adae2d && git merge --no-ff 225ad588f7ac6ff238f6e770d8ab5ee59cf2d9de
node scripts/docs-audit/affected-docs.mjs --json c744c0af332c31748ab7efc87e87505277adae2d
|
Contract review — PR #17892 (card #17329)Head reviewed:
① Derived judgments — every acceptance-set and public-surface change, named and judged1. The clause-② act is a new ipc message on a published command's contract — 2. No new PUBLISHED package symbol. — correct, measured, and it is the third time today that this same reading decided a verdict. 3. 4. ⭐ The keying is the delivery's whole argument, and it is right. — 5. The ordering guarantee is a latch, not a sleep, and it holds in both clock orders. — correct. 6. Rider 2 delivered: the over-budget transcript is no longer byte-identical to a boot that declared no seeds. The banner prints ② semver level —
|
⛔ A sequencing slip of this seat's, recorded because the remedy is one line and the next seat will hit it
What happened, on the clock:
⭐ The carrier clear RE-TRIGGERS ⇒ the remedy, and it belongs in the landing pre-check itself: ③ is re-taken after the carrier clear, not before it — 「⛔ every check green, not the required subset」 means at the moment of the flip, and a clear that fires a gate makes the earlier reading stale by its own hand. ⛔ Nothing here is walked back: the re-run came back success, the whole head reads 31 Generated by Claude Code |
Dequeued at 19:42:53Z by a gate that could not READ, not by a failure in this PR — re-queued once, and that re-run is spent
⭐ This is the gate working, and it is the fail-closed posture this repo chose on purpose. It did not judge this PR's carrier absent or present; it could not fetch the label set at all, and it refuses rather than shrugs — because a label that cannot be seen and a label that was stripped are the same bytes to a reader that treats an unreadable answer as a clean one. ⇒ Not this PR's failure, and ⛔ nothing here is "fixed" by changing this PR. Measured at 19:44Z: the head Action taken: re-queued once, which is the remedy the gate names in its own text. ⛔ That re-run is now spent. If the same leg refuses a second time it is not a transient and this seat will say what is blocking rather than re-queue again — 「'flake' is not a root cause」, and a gate that cannot read the API twice in a row is a report, not a retry. ⛔ And the two things that would make this go away without earning it are ⛔ neither of them available: stripping a label to get past the check is the defect the leg was built from, and 「edit this check」 is refused in the same paragraph. Generated by Claude Code |
Fixes #17329
Clause-②: yes
os servenow announcesobjectstack:seed-settledon its existing ipc channel when this boot's seeding has come to rest, andos devforwards it to its own parent when one holds the channel. Implements decision batch #118 item 5 (D1) as ruled at5642795097, plus its banner rider.⭐ The producer already existed.
@objectstack/runtimedeclares every seed source and settles it the moment its boot-time write is done, publishing the tally under@objectstack/spec'sseed-settlementcontract. This PR is the hop outward — it registers no service, mutates no tally, and changes nothing inpackages/runtimeorpackages/spec. It subscribes to two hooks the kernel already fires and reads a snapshot it already publishes.Every site was re-derived from its SYMBOL
Every line number on the card was stale twice over, so nothing was carried forward. Re-read on
origin/mainat272c04b46:publishBoundPort/runtimeBoundPortChannelsinpackages/cli/src/commands/serve.tsprintServerReadyinpackages/cli/src/utils/format.ts, called through a thunk fromserve.tsos dev's roleserve --devoverstdio: ['inherit','inherit','inherit','ipc']; consumes the listening message, emits no banneremitSeedSettledinpackages/runtime/src/app-plugin.ts,declareSeedSourceinpackages/runtime/src/seed-settlement.ts— read only, both untouched⭐ Ruled item 3, MEASURED on a real boot
The gate: multi-tenant replay and
skipSeedDatareportpending > 0for the whole boot, so a consumer waiting on the new message must not hang forever there.⛔ The predicate is
inFlight === 0, notpending === 0.suppress()moves a source out of the in-flight tally and records why, so both modes reachinFlight === 0inside Phase 2start()whilependingstays above zero forever. Apending-keyed message would never be sent on those boots, and its absence would be indistinguishable from a boot still writing — the same ambiguity this card exists to end, one level up.Measured,
os devunderOS_TENANCY_POSTURE=groupwith the org runtime declared by the host app, a real ipc parent recording with the clock:The message arrived 1 ms after the banner on a boot whose
pendingnever reaches zero. The consumer does not hang. Reaching that boot needed three refusals satisfied in order — the host app must declare@objectstack/organizations(#4719: merely reachable is rejected),OS_PLATFORM_OWNER_EMAIL, andOS_AUTH_MEMBERSHIP_POLICY. The app-manifest declaration was made temporarily for the measurement and reverted;package.jsonandpnpm-lock.yamlwere both restored to theirHEADblob hashes with a clean whole-treegit status, and this PR carries neither file.skip-seed-datawas NOT reached on a real boot, and that is a code-path fact rather than a gap. It is set only bycreateStandaloneStackfromos migrate's planning path (packages/cli/src/utils/schema-migrate.ts);servenever sets it, so noos serve/os devboot can enter that branch. It is pinned structurally against the contract's ownSeedSettlementSnapshot, with the producer-side pin inpackages/runtime/src/app-plugin.seed.test.tsgreen in the same session as the control (14/14).The other two clocks, also measured
+13.728s+13.729s,ok: true, 132 rowsOS_INLINE_SEED_BUDGET_MS=1)+8.938s+8.940s— the budget WARN fired, the continuation still finished first+8.533s+8.535s,ok: false,110 ok / 22 errorssqlite,sqlite-wasmandmemorydrivers: on this container the seed completes during the remaining seconds of plugin startup, so the banner always lost the race in the reachable direction. That state was measured on this card earlier ({"pending":1,"inFlight":1}read at banner time,5636189867); here it is driven as a unit with an ablation instead of waited for.Ordering, and why this is not a fourth bound-port channel
The settle is latched and released after
publishBoundPorthas driven its three channels, so a parent that waits forobjectstack:listeningand only then listens for the settle cannot miss one that happened duringruntime.start(). ⛔ It is deliberately not folded intopublishBoundPort: those three are one ordered publication of ONE number, and this is a different fact on a different clock that frequently has not happened yet. ThepublishBoundPortcall-site pin still reads exactly two mentions in code.Ablation — three legs, each restored by blob hash
Run under the shared verify lock; every mutation proven on disk (anchor uniqueness before, old/new counts and a moved blob hash after) before its verdict was read.
pendingpredicateinFlight === 0→pending === 0releasedgatekernel:readyhookEach leg restored with
git checkout HEAD -- PATH, restored blob8fa02a659e8cae77207e0e326cbb51ad69e2ceebmatching HEAD in all three, and a clean whole-treegit statusat the end.Rider 2 — the banner no longer omits seeding
Seeds:is fed by outcomes recorded when a load finishes, so past the budget the row was absent and the transcript was byte-identical to an app that declares no seeds — which is how the defect hid. It now readspending — N sources still writingwith a line saying seeding continues in the background; suppressed sources are named instead (not run this boot (multi-tenant-replay), as measured above). The ablation for this half is in the test file: the same options without the reading reproduce the zero-seed-row transcript.Verification
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, reconciled against the run list) — all green. Two first returned a prerequisite rather than a verdict (check:skill-examplesexit 1,check:dual-build-cjs-loadsexit 3 = PREREQUISITE NOT MET, both naming unbuilt packages); after building@objectstack/client-reactand@objectstack/organizationsboth re-ran exit 0. Neither was read as a pass or a finding in between.pnpm lint, the FULL union (eslint . --no-inline-config, ⛔ not narrowed) — exit 0 captured before any pipe; 6672 files counted from eslint's own--format json; 0 errors, 0 warnings. Positive control: all 6 changed source files appear by name in eslint's own output.pnpm --filter @objectstack/cli exec vitest run --project unit— 204 files / 2935 tests passed.pnpm --filter @objectstack/cli exec vitest run --project integration— 45 files / 397 tests passed (run locally rather than declared to CI, because this diff touches the kernel startup path).pnpm --filter @objectstack/cli typecheck— exit 0, includingcheck:test-typecheck.pnpm --filter '@objectstack/cli^...' build— exit 0.pnpm --filter @objectstack/runtime exec vitest run src/app-plugin.seed.test.ts— 14/14, pinning the exactsuppressedsnapshots this message consumes.All heavy runs serialised through
scripts/pm/os-verify-lock.sh; every verdict read from the wrapper's ownVERDICT command-exitline, never a bare exit status.Acceptance notes
os devandos serveare not symmetric, and the docs now say so.os devconsumesobjectstack:listeningitself — it is how the↪ server bound to portline and the MCP connect hint learn the real port — and relays only the settle message. Measured: an ipc parent ofos devreceives exactly one message. Widening that was not ruled and is not attempted here; a consumer that needs both spawnsos serve. Noted, not filed — no open PR or queued card touches this surface.trackPlugined: that list feeds the banner'sPlugins:count and name row, and an internal subscriber does not belong on a published banner.packages/specandpackages/runtimewere read and never edited, as ordered. Nothing in the design needed either changed.🤖 Generated with Claude Code
https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
Generated by Claude Code